<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>绘制带箭头的直线和折线</title>
    <style>
      * {
        padding: 0;
        margin: 0;
        list-style: none;
        box-sizing: border-box;
      }
      .board {
        border: 1px solid;
        margin: 30px;
      }
    </style>
  </head>
  <body>
    <div>
      <!-- <canvas class="board" width="1024" height="768"> -->
      <canvas class="board" id="canvas">
        当前浏览器不支持 canvas，请更换浏览器再试！
      </canvas>
    </div>
  </body>
  <script>
    /** @type {HTMLCanvasElement} */
    const canvas = document.getElementById("canvas");
    const ctx = canvas.getContext("2d");
    canvas.width = 600;
    canvas.height = 700;

    // 线JSON文件举例：
    var line = [
      {
        start: {
          x: 325,
          y: 70,
        },
        end: {
          x: 425,
          y: 70,
          lineText: "892eyui",
        },
        linecolor: "#5f5fff",
      },
      {
        start: {
          x: 259,
          y: 100,
        },
        turnPoint: [
          {
            x: 159,
            y: 100,
            lineText: "今天是个11333好天气",
            linecolor: "#f0f",
          },
          {
            x: 159,
            y: 170,
            lineText: "今天是个22333好天气",
          },
          {
            x: 129,
            y: 69,
            lineText: "今天是个33333好天气",
          },
        ],
        end: {
          x: 139,
          y: 294,
          lineText: "END",
        },
        linecolor: "#f00",
      },
      {
        start: {
          x: 105,
          y: 60,
        },
        end: {
          x: 225,
          y: 90,
          lineText: "892eyui",
        },
        linecolor: "#0ff",
      },
    ];

    useDrawLine(ctx, line, {
      fontFamily: "Times New Roman",
      lineColor: "#f99",
    });
    /**
     * 函数配置
     * ctx = document.querySelector("canvas").getContext("2d");
     * linePointArrJson: 各条线组成的数组 ====> Array<LinePointJsonObject> 具体见下
     * canvasConfig: 整体配置项 ====> CanvasConfig 具体见下
     * 
        // 整个线的 JSON 定义格式为 Array<LinePointJsonObject>
        // 定义其中一个折线 JSON
        interface LinePointJsonObject {
            start: StartPoint; // 起点
            end: CommonPoint; // 终点
            turnPoint?: CommonPoint[]; // 转折点
            linecolor?: string; // 其中一条整线的线条颜色
        }

        // 起始点
        interface StartPoint {
            x: number; // 点 x 坐标
            y: number; // 点 y 坐标
        }

        // 转折点和末尾点
        interface CommonPoint extends StartPoint {
            lineText?: string; // 写在线上字的内容
        }

        // 整体配置
        interface CanvasConfig {
            lineColor: string | Array<string>; // 设定所有线条颜色（或者每一条颜色）
            fontSize: number; // 设定所有字体大小
            fontFamily: string; // 设定所有字体
            pointSize: number; // 设定所有点大小
            arrowSize: number; // 设定所有箭头大小
            fontMaxWidth: number; // 设定所有字体最大宽度
            lineWidth: number; // 设定所有线条宽度
            textOffset: number; // 设定所有文字距离线段偏移量
        }
     */
    function useDrawLine(ctx, linePointArrJson, canvasConfig = {}) {
      // 配置项，并设置默认值
      let {
        lineColor = "#000",
        fontSize = 13,
        pointSize = 4,
        arrowSize = 10,
        fontMaxWidth = 150,
        textOffset = "-5",
        lineWidth = 1,
        fontFamily = "微软雅黑",
      } = canvasConfig;

      // 开始绘制
      ctx.save();
      ctx.lineWidth = lineWidth;
      for (let i = 0; i < linePointArrJson.length; i++) {
        // 确定线的颜色，如果没设置，则使用默认颜色
        ctx.save();
        if (typeof lineColor === "string") {
          !linePointArrJson[i].linecolor &&
            (linePointArrJson[i].linecolor = lineColor);
        } else {
          if (lineColor.length !== linePointArrJson.length) {
            throw new Error("线颜色数组必须与线组成的数组长度相同！");
          }
          !linePointArrJson[i].linecolor &&
            (linePointArrJson[i].linecolor = lineColor[i]);
        }
        ctx.restore();

        if (
          !(
            linePointArrJson[i].turnPoint &&
            linePointArrJson[i].turnPoint.length
          )
        ) {
          // 如果没有转折点的话
          // 画线
          ctx.beginPath();

          ctx.moveTo(linePointArrJson[i].start.x, linePointArrJson[i].start.y);
          // 画圆形线头
          ctx.arc(
            linePointArrJson[i].start.x,
            linePointArrJson[i].start.y,
            pointSize,
            Math.atan2(
              linePointArrJson[i].end.y - linePointArrJson[i].start.y,
              linePointArrJson[i].end.x - linePointArrJson[i].start.x
            ),
            Math.atan2(
              linePointArrJson[i].end.y - linePointArrJson[i].start.y,
              linePointArrJson[i].end.x - linePointArrJson[i].start.x
            ) +
              2 * Math.PI
          );

          // 画线
          ctx.lineTo(linePointArrJson[i].end.x, linePointArrJson[i].end.y);
          ctx.closePath();
          ctx.fillStyle = linePointArrJson[i].linecolor; // 线头填充颜色
          ctx.strokeStyle = linePointArrJson[i].linecolor; // 线颜色
          ctx.fill();
          ctx.stroke();

          // 画箭头
          ctx.beginPath();
          // 任意方向，箭头在线指向的尾部
          // 注意，下面这个 arrowSize 就是箭头长度
          ctx.lineTo(
            linePointArrJson[i].end.x -
              arrowSize *
                Math.cos(
                  Math.PI / 4 -
                    Math.atan2(
                      linePointArrJson[i].end.y - linePointArrJson[i].start.y,
                      linePointArrJson[i].end.x - linePointArrJson[i].start.x
                    )
                ),
            linePointArrJson[i].end.y +
              arrowSize *
                Math.sin(
                  Math.PI / 4 -
                    Math.atan2(
                      linePointArrJson[i].end.y - linePointArrJson[i].start.y,
                      linePointArrJson[i].end.x - linePointArrJson[i].start.x
                    )
                )
          );
          ctx.lineTo(
            linePointArrJson[i].end.x -
              arrowSize *
                Math.sin(
                  Math.PI / 4 -
                    Math.atan2(
                      linePointArrJson[i].end.y - linePointArrJson[i].start.y,
                      linePointArrJson[i].end.x - linePointArrJson[i].start.x
                    )
                ),
            linePointArrJson[i].end.y -
              arrowSize *
                Math.cos(
                  Math.PI / 4 -
                    Math.atan2(
                      linePointArrJson[i].end.y - linePointArrJson[i].start.y,
                      linePointArrJson[i].end.x - linePointArrJson[i].start.x
                    )
                )
          );
          ctx.lineTo(linePointArrJson[i].end.x, linePointArrJson[i].end.y);
          ctx.closePath();
          ctx.fillStyle = linePointArrJson[i].linecolor; // 箭头填充颜色
          ctx.fill();
          ctx.stroke();
          // 写字
          ctx.font = fontSize + "px " + fontFamily;
          ctx.textAlign = "center";
          ctx.textBaseline = "bottom";
          ctx.fillStyle = linePointArrJson[i].linecolor; // 字的颜色
          // 文字出现位置
          ctx.save();
          // 将canvas的旋转中点更换为现在文字所在的位置
          ctx.translate(
            (linePointArrJson[i].start.x + linePointArrJson[i].end.x) / 2,
            (linePointArrJson[i].start.y + linePointArrJson[i].end.y) / 2
          );
          ctx.rotate(
            Math.atan2(
              Math.abs(linePointArrJson[i].end.y - linePointArrJson[i].start.y),
              Math.abs(linePointArrJson[i].end.x - linePointArrJson[i].start.x)
            )
          );
          ctx.fillText(
            linePointArrJson[i].end.lineText ?? "",
            0, // 由于这时canvas的旋转中点已经更换，因此就直接写 0 0 就行
            textOffset, // 偏移量
            fontMaxWidth
          );
          ctx.restore(); // 加载默认颜色配置
        } else {
          // 如果有转折点的话
          // 画线
          ctx.beginPath();
          // 画圆形线头
          ctx.moveTo(linePointArrJson[i].start.x, linePointArrJson[i].start.y);
          ctx.arc(
            linePointArrJson[i].start.x,
            linePointArrJson[i].start.y,
            pointSize,
            Math.atan2(
              linePointArrJson[i].turnPoint[0].y - linePointArrJson[i].start.y,
              linePointArrJson[i].turnPoint[0].x - linePointArrJson[i].start.x
            ),
            Math.atan2(
              linePointArrJson[i].turnPoint[0].y - linePointArrJson[i].start.y,
              linePointArrJson[i].turnPoint[0].x - linePointArrJson[i].start.x
            ) +
              2 * Math.PI
          );
          ctx.lineTo(
            linePointArrJson[i].turnPoint[0].x,
            linePointArrJson[i].turnPoint[0].y
          );
          ctx.closePath();
          ctx.fillStyle = linePointArrJson[i].linecolor; // 线头填充颜色
          ctx.strokeStyle = linePointArrJson[i].linecolor; // 线颜色
          ctx.fill();
          ctx.stroke();

          // 写字
          ctx.font = fontSize + "px " + fontFamily;
          ctx.textAlign = "center";
          ctx.textBaseline = "bottom";
          ctx.fillStyle = linePointArrJson[i].linecolor; // 字的颜色
          // 文字出现位置
          ctx.save(); // 保存默认状态/保存当前状态
          // 将canvas的旋转中点更换为现在文字所在的位置
          ctx.translate(
            (linePointArrJson[i].start.x + linePointArrJson[i].turnPoint[0].x) /
              2,
            (linePointArrJson[i].start.y + linePointArrJson[i].turnPoint[0].y) /
              2
          );
          ctx.rotate(
            Math.atan2(
              Math.abs(
                linePointArrJson[i].start.y - linePointArrJson[i].turnPoint[0].y
              ),
              Math.abs(
                linePointArrJson[i].start.x - linePointArrJson[i].turnPoint[0].x
              )
            )
          );
          // 文字出现位置
          ctx.fillText(
            linePointArrJson[i].turnPoint[0].lineText ?? "",
            0,
            textOffset, // 偏移量
            fontMaxWidth
          );
          ctx.restore(); // 加载默认颜色配置

          for (let j = 0; j < linePointArrJson[i].turnPoint.length; j++) {
            // 转折点
            ctx.beginPath();
            ctx.moveTo(
              linePointArrJson[i].turnPoint[j].x,
              linePointArrJson[i].turnPoint[j].y
            );
            if (j < linePointArrJson[i].turnPoint.length - 1) {
              ctx.lineTo(
                linePointArrJson[i].turnPoint[j + 1].x,
                linePointArrJson[i].turnPoint[j + 1].y
              );

              // 文字
              ctx.save();
              ctx.translate(
                (linePointArrJson[i].turnPoint[j].x +
                  linePointArrJson[i].turnPoint[j + 1].x) /
                  2,
                (linePointArrJson[i].turnPoint[j].y +
                  linePointArrJson[i].turnPoint[j + 1].y) /
                  2
              );
              ctx.rotate(
                Math.atan2(
                  Math.abs(
                    linePointArrJson[i].turnPoint[j + 1].y -
                      linePointArrJson[i].turnPoint[j].y
                  ),
                  Math.abs(
                    linePointArrJson[i].turnPoint[j + 1].x -
                      linePointArrJson[i].turnPoint[j].x
                  )
                )
              );
              // 文字出现位置
              ctx.fillText(
                linePointArrJson[i].turnPoint[j + 1].lineText ?? "",
                0,
                textOffset, // 偏移量
                fontMaxWidth
              );
              ctx.restore();
            } else {
              ctx.save();
              ctx.translate(
                (linePointArrJson[i].turnPoint[j].x +
                  linePointArrJson[i].end.x) /
                  2,
                (linePointArrJson[i].turnPoint[j].y +
                  linePointArrJson[i].end.y) /
                  2
              );
              ctx.rotate(
                Math.atan2(
                  Math.abs(
                    linePointArrJson[i].end.y -
                      linePointArrJson[i].turnPoint[j].y
                  ),
                  Math.abs(
                    linePointArrJson[i].end.x -
                      linePointArrJson[i].turnPoint[j].x
                  )
                )
              );
              // 文字出现位置
              ctx.fillText(
                linePointArrJson[i].end.lineText ?? "",
                0,
                textOffset, // 偏移量
                fontMaxWidth
              );
              ctx.restore();
              ctx.lineTo(linePointArrJson[i].end.x, linePointArrJson[i].end.y);
            }
            ctx.closePath();
            ctx.stroke();
          }

          // 画箭头
          ctx.beginPath();
          // 任意方向，箭头在线指向的尾部
          // 注意，下面这个 arrowSize 就是箭头长度
          ctx.lineTo(
            linePointArrJson[i].end.x -
              arrowSize *
                Math.cos(
                  Math.PI / 4 -
                    Math.atan2(
                      linePointArrJson[i].end.y -
                        linePointArrJson[i].turnPoint[
                          linePointArrJson[i].turnPoint.length - 1
                        ].y,
                      linePointArrJson[i].end.x -
                        linePointArrJson[i].turnPoint[
                          linePointArrJson[i].turnPoint.length - 1
                        ].x
                    )
                ),
            linePointArrJson[i].end.y +
              arrowSize *
                Math.sin(
                  Math.PI / 4 -
                    Math.atan2(
                      linePointArrJson[i].end.y -
                        linePointArrJson[i].turnPoint[
                          linePointArrJson[i].turnPoint.length - 1
                        ].y,
                      linePointArrJson[i].end.x -
                        linePointArrJson[i].turnPoint[
                          linePointArrJson[i].turnPoint.length - 1
                        ].x
                    )
                )
          );
          ctx.lineTo(
            linePointArrJson[i].end.x -
              arrowSize *
                Math.sin(
                  Math.PI / 4 -
                    Math.atan2(
                      linePointArrJson[i].end.y -
                        linePointArrJson[i].turnPoint[
                          linePointArrJson[i].turnPoint.length - 1
                        ].y,
                      linePointArrJson[i].end.x -
                        linePointArrJson[i].turnPoint[
                          linePointArrJson[i].turnPoint.length - 1
                        ].x
                    )
                ),
            linePointArrJson[i].end.y -
              arrowSize *
                Math.cos(
                  Math.PI / 4 -
                    Math.atan2(
                      linePointArrJson[i].end.y -
                        linePointArrJson[i].turnPoint[
                          linePointArrJson[i].turnPoint.length - 1
                        ].y,
                      linePointArrJson[i].end.x -
                        linePointArrJson[i].turnPoint[
                          linePointArrJson[i].turnPoint.length - 1
                        ].x
                    )
                )
          );
          ctx.lineTo(linePointArrJson[i].end.x, linePointArrJson[i].end.y);
          ctx.closePath();
          ctx.fillStyle = linePointArrJson[i].linecolor; // 箭头填充颜色
          ctx.fill();
          ctx.stroke();
        }
      }
      ctx.restore();
    }
  </script>
</html>
